﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;

namespace DeSleeper.Library.Tasks
{
    public class RemoteProcess : Component
    {
        private const string EXECUTABLE_NAME = "rctrlx.exe";

        /// <summary>
        /// Starts the process resource that is specified by the parameter containing
        /// process start information (for example, the file name of the process to start)
        /// and associates the resource with a new Process component. 
        /// </summary>
        /// <param name="startInfo"></param>
        /// <param name="machineName"></param>
        /// <remarks>
        /// <see cref="RemoteProcess"/> does not support Verb, UseShellExecute,
        /// LoadUserProfile or EnvironmentVariables.  These properties are ignored by
        /// <see cref="Start"/>.
        /// </remarks>
        /// <returns></returns>
        public static RemoteProcess Start(ProcessStartInfo startInfo, string machineName)
        {
            var options = new StringBuilder();
            if (!string.IsNullOrEmpty(startInfo.WorkingDirectory))
                options.Append(" /w \"").Append(startInfo.WorkingDirectory).Append('"');

            var rctrlStartInfo =
                new ProcessStartInfo
                    {
                        FileName = EXECUTABLE_NAME,
                        UseShellExecute = false,
                        ErrorDialog = false,
                        CreateNoWindow = startInfo.CreateNoWindow,
                        RedirectStandardError = startInfo.RedirectStandardError,
                        RedirectStandardInput = startInfo.RedirectStandardInput,
                        RedirectStandardOutput = startInfo.RedirectStandardOutput,
                        StandardErrorEncoding = startInfo.StandardErrorEncoding,
                        StandardOutputEncoding = startInfo.StandardOutputEncoding,
                        WindowStyle = startInfo.WindowStyle,
                        UserName = startInfo.UserName,
                        Password = startInfo.Password,
                        Domain = startInfo.Domain,
                        Arguments = string.Format("{0}{1} /app \"{2}\" {3}",
                        machineName,
                        options,
                        startInfo.FileName,
                        startInfo.Arguments)
                    };

            
            var proxyProcess = Process.Start(rctrlStartInfo);
            return new RemoteProcess(proxyProcess)
                       {
                           MachineName = machineName,
                           ProcessName = Path.GetFileNameWithoutExtension(startInfo.FileName)
                       };
        }

        public static void StartWithoutWait(ProcessStartInfo startInfo, string machineName)
        {
            var options = new StringBuilder(" /d");
            if (!string.IsNullOrEmpty(startInfo.WorkingDirectory))
                options.Append(" /w ").Append(startInfo.WorkingDirectory);

            var rctrlStartInfo =
                new ProcessStartInfo
                {
                    FileName = EXECUTABLE_NAME,
                    UseShellExecute = false,
                    ErrorDialog = false,
                    CreateNoWindow = true,
                    RedirectStandardError = startInfo.RedirectStandardError,
                    RedirectStandardInput = startInfo.RedirectStandardInput,
                    RedirectStandardOutput = startInfo.RedirectStandardOutput,
                    StandardErrorEncoding = startInfo.StandardErrorEncoding,
                    StandardOutputEncoding = startInfo.StandardOutputEncoding,
                    WindowStyle = startInfo.WindowStyle,
                    UserName = startInfo.UserName,
                    Password = startInfo.Password,
                    Domain = startInfo.Domain,
                    Arguments = string.Format(@"{0}{1} /app {2} {3}",
                    machineName,
                    options,
                    startInfo.FileName,
                    startInfo.Arguments)
                };

            using(Process.Start(rctrlStartInfo)) {}
        }

        public string MachineName { get; private set; }
        public string ProcessName { get; private set; }
        public event EventHandler Exited;
        public event DataReceivedEventHandler ErrorDataReceived;
        public event DataReceivedEventHandler OutputDataReceived;
        private readonly Process _proxyProcess;

        private RemoteProcess(Process proxyProcess)
        {
            _proxyProcess = proxyProcess;
            _proxyProcess.Exited += proxyExited;
            _proxyProcess.ErrorDataReceived += proxyErrorDataReceived;
            _proxyProcess.OutputDataReceived += proxyOutputDataReceived;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_proxyProcess != null)
                {
                    _proxyProcess.Exited -= proxyExited;
                    _proxyProcess.ErrorDataReceived -= proxyErrorDataReceived;
                    _proxyProcess.OutputDataReceived -= proxyOutputDataReceived;
                }
            }
            base.Dispose(disposing);
        }

        void proxyOutputDataReceived(object sender, DataReceivedEventArgs e)
        {
            onDataReceived(e, OutputDataReceived);
        }

        void proxyErrorDataReceived(object sender, DataReceivedEventArgs e)
        {
            onDataReceived(e, ErrorDataReceived);
        }

        private void proxyExited(object sender, EventArgs e)
        {
            OnExited();
        }

        protected void OnExited()
        {
            if (Exited != null)
                Exited(this, EventArgs.Empty);
        }

        private void onDataReceived(DataReceivedEventArgs args, DataReceivedEventHandler handler)
        {
            if (handler != null)
                handler(this, args);
        }

        public void Kill()
        {
            _proxyProcess.Kill();
        }

        public void WaitForExit()
        {
            _proxyProcess.WaitForExit();
        }

        public bool WaitForExit(int milliseconds)
        {
            return _proxyProcess.WaitForExit(milliseconds);
        }

        public bool WaitForInputIdle()
        {
            return _proxyProcess.WaitForInputIdle();
        }

        public bool WaitForInputIdle(int milliseconds)
        {
            return _proxyProcess.WaitForInputIdle(milliseconds);
        }

        public void ThrowOn(Func<int, bool> isExitCodeError)
        {
            ProcessExtensions.throwOn(isExitCodeError, _proxyProcess, ProcessName,
                    "Remote process {0} failed: \n{1}",
                    "Remote process {0} failed with exit code {1}.");
        }
    }

    public static class ProcessExtensions
    {
        public static void ThrowOn(this Process process, Func<int, bool> isExitCodeError)
        {
            throwOn(isExitCodeError, process, process.ProcessName,
                    "Process {0} failed: \n{1}",
                    "Process {0} failed with exit code {1}.");
        }

        internal static void throwOn(Func<int, bool> isExitCodeError, Process process, 
            string processName,
            string messageFormat, string emptyMessageFormat)
        {
            if (isExitCodeError(process.ExitCode))
            {
                var message = string.Empty;
                if (process.StartInfo.RedirectStandardError)
                    message += process.StandardError.ReadToEnd();
                if (process.StartInfo.RedirectStandardOutput)
                    message += process.StandardOutput.ReadToEnd();
                
                if (string.IsNullOrEmpty(message))
                {
                    message = string.Format(
                        emptyMessageFormat,
                        processName, process.ExitCode);
                }
                else
                {
                    message = string.Format(
                        messageFormat,
                        processName, message);
                }

                throw new InvalidOperationException(message);
            }
        }
    }
}
